package com.cis467.mrball;

import java.util.ArrayList;

import com.cis467.mrball.backend.GameManagement;
import com.cis467.mrball.parts.Ball;
import com.cis467.mrball.parts.Button;
import com.cis467.mrball.parts.Floor;
import com.cis467.mrball.parts.Level;
import com.cis467.mrball.parts.Portal;
import com.cis467.mrball.parts.Wall;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Canvas;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.Toast;

public class MrBallView extends SurfaceView implements SurfaceHolder.Callback {
	class MrBallThread extends Thread implements SensorEventListener{

		/*
		 * Keys for saving state
		 */
		private static final String KEY_X = "mX";
		private static final String KEY_Y = "mY";
		private static final String KEY_DX = "mDX";
		private static final String KEY_DY = "mDY";

		/*
		 * State-tracking constants
		 */
		private static final int STATE_LOADING = 0;
		private static final int STATE_PAUSE = 1;
		private static final int STATE_READY = 2;
		private static final int STATE_RUNNING = 3;
		private static final int STATE_WIN = 4;
		private static final int TUTORIAL_END = 5;
		
		private static final int RUN_DELAY = 100;
		
		/*
		 * Physics settings
		 */
		private static final double GRAVITY_SCALAR = 1.8;
		private static final double MAX_SPEED = 18.0;
		private static final double MIN_SPEED = 0.0003;
		private static final double FRICTION = 0.90;
		private static final double DAMPEN = 0.994;
		private static final double BOOST = 1.004;
		private static final double BOUNCE = -0.4;
		private static final double TIME_STEP = .001;
		
		/*
		 * Vibration constants
		 */
		private static final double IMPULSE_THRESHOLD = 0.4;
		private static final long TIME_WALL_BASE = 5;
		private static final long TIME_PIT = 75;
		private static final long TIME_LASER = 100;
		private static final long TIME_PORTAL = 10;
		private static final long TIME_BUTTON = 20;
		private static final long TIME_KEY = 150;
		private static final long TIME_END = 5;

		private MrBallView mParent;

		/** Handle to the surface manager object we interact with */
		private SurfaceHolder mSurfaceHolder;

		private SensorManager mSensorManager;
		
		private Vibrator mVibrator;

		private MrBallScreenManager mScreenManager;

		private ArrayList<Level> mGame;
		
		private String mode;

		private Level mLevel;

		private Ball mBall;

		/** Indicate whether the surface has been created & is ready to draw */
		private boolean mRun = false;

		/** The state of the game. One of READY, RUNNING, PAUSE, WIN, GAMEOVER */
		private int mStatus;

		/** Used to figure out elapsed time between frames */
		private long mLastUpdateTime;
		private long mGameTime;

		
		private long mLastSensorUpdateTime = -1;
		
		private double mXGravity;
		private double mYGravity;
		private double mXTare;
		private double mYTare;
//		private boolean mMirrorY;
//		private int mRotationsClockwise;

		public MrBallThread(MrBallView parent, SurfaceHolder surfaceHolder, Context context) {

			mParent = parent;

			mContext = context;
			
			mSurfaceHolder = surfaceHolder;
			mSurfaceHolder.setKeepScreenOn(true);
			
			mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
			
			mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
			mScreenManager = new MrBallScreenManager(mContext);

			mXGravity = 0.0;
			mYGravity = 0.0;
			
			SharedPreferences mCalPrefs = 
					mContext.getSharedPreferences(SharedConstants.CALIBRATION_PREFS, Context.MODE_PRIVATE);
			
			mXTare = mCalPrefs.getFloat(SharedConstants.TARE_X, (float) 0.0);
			mYTare = mCalPrefs.getFloat(SharedConstants.TARE_Y, (float) 0.0);
			
//	        mMirrorY = mScreenPrefs.getBoolean(SharedConstants.MIRROR_Y, false);
//	        mRotationsClockwise = mScreenPrefs.getInt(SharedConstants.ROTATIONS_CLOCK, 0);
		}

		/**
		 * Starts the game, setting initial parameters
		 */
		public void initialize(ArrayList<Level> game, String mode) {

			setState(STATE_LOADING);
			
			this.mode = mode;
			
			mGame = game;

			mLevel = mGame.remove(0);

			synchronized (mSurfaceHolder) {

				loadLevel(mLevel);

				thread.registerListener();
			}
		}

		private void loadLevel(Level level) {

			setState(STATE_LOADING);
			
			try {
				level.setup(false);
			}
			catch(Exception e) {
				Toast.makeText(mContext, "Level setup failed.", Toast.LENGTH_SHORT).show();
			}

			mLevel = level;

			mScreenManager.prerenderLevel(mLevel);

			mBall = new Ball((mLevel.start.col + 0.5),(mLevel.start.row + 0.5), 0, 0);

			mLastUpdateTime = System.currentTimeMillis() + 100;

			mGameTime = 0;
			
			setState(STATE_RUNNING);
		}

		public void restartLevel() {

			mBall.stop();
			mBall.moveTo((mLevel.start.col + 0.5),(mLevel.start.row + 0.5));

			if(mLevel.key != null) {

				mLevel.key.isVisible = true;
				mLevel.end.isVisible = false;

			}
			else {

				mLevel.end.isVisible = true;
			}

			for(Button b : mLevel.buttons) {
				b.isVisible = true;
				b.p1.isVisible = false;
				b.p2.isVisible = false;
			}

			mGameTime = 0;
		}

		/**
		 * Pauses the physics update & animation.
		 */
		public void pause() {
			synchronized (mSurfaceHolder) {
				if (mStatus == STATE_RUNNING) {
					setState(STATE_PAUSE);
					unregisterListener();
				}
			}
		}

		/**
		 * Resumes from a pause.
		 */
		public void unpause() {
			// Move the real time clock up to now
			synchronized (mSurfaceHolder) {
				registerListener();
			}
			setState(STATE_RUNNING);
		}

		@Override
		public void run() {
			while (mRun) {
				Canvas c = null;
				try {
					c = mSurfaceHolder.lockCanvas(null);
					synchronized (mSurfaceHolder) {
						if(mStatus == STATE_LOADING) {
							mScreenManager.clearCanvas();
						}
						if (mStatus == STATE_RUNNING) { 
							
							if (mLastUpdateTime < System.currentTimeMillis()) {
								updatePhysics();
								mScreenManager.draw(c, mLevel, mBall, formatElapsed(mGameTime));
							}
						}
						else if(mStatus == STATE_WIN) {

							mScreenManager.drawWinScreen(c, formatElapsed(mGameTime));
						}
						else if(mStatus == TUTORIAL_END) {
							mScreenManager.drawGameOverScreen(c);
						}
					}
				} finally {
					// do this in a finally so that if an exception is thrown
					// during the above, we don't leave the Surface in an
					// inconsistent state
					if (c != null) {
						mSurfaceHolder.unlockCanvasAndPost(c);
					}
				}
			}
		}

		public void handleTouch(int x, int y) {

			// Check if the user touched the side panel
	    	if(mScreenManager.isSideMenuTouch(x)) {
	    		
				if(mStatus == STATE_WIN || mStatus == TUTORIAL_END) {
					
					//GameManagement.new_highScore(new HighScore(mLevel.getName(), GameManagement.get_playerID(), mGameTime));
				
		    		if(mScreenManager.isContinueButton(y)) {
		    			
		    			if(!mode.equals("Tutorial") || mStatus == TUTORIAL_END) {
							setRunning(false);
							mParent.finish();
						} 
		    			else if(mGame.size() > 0) {
							loadLevel(mGame.remove(0));
							setState(STATE_RUNNING);
						}
		    			else {
							setState(TUTORIAL_END);
						}
		    		}
		    		
		    		else if(mScreenManager.isRestartButton(y)) {
		    			restartLevel();
		    			setState(STATE_RUNNING);
		    		}
		    		
		    		else if(mScreenManager.isExitButton(y)) {
		    			setRunning(false);
						mParent.finish();
		    		}
				}
	    	}
		}

		private String formatElapsed(long elapsed) {
			// Return as string with 2 decimal places
			return "" + Math.floor(elapsed/10.0)/100.0;
		}

		/**
		 * Used to signal the thread whether it should be running or not.
		 * Passing true allows the thread to run; passing false will shut it
		 * down if it's already running. Calling start() after this was most
		 * recently called with false will result in an immediate shutdown.
		 * 
		 * @param b true to run, false to shut down
		 */
		public void setRunning(boolean b) {
			mRun = b;
		}

		/**
		 * Sets the game mode. That is, whether we are running, paused, in the
		 * failure state, in the victory state, etc.
		 * 
		 * @see #setState(int, CharSequence)
		 * @param mode one of the STATE_* constants
		 */
		public void setState(int mode) {
			synchronized (mSurfaceHolder) {
				mStatus = mode; 
				
				if(mode == STATE_RUNNING)
					mLastUpdateTime = System.currentTimeMillis() + RUN_DELAY;
			}
		}

		/* Callback invoked when the surface dimensions change. */
		public void setSurfaceSize(int width, int height) {
			// synchronized to make sure these all change atomically
			synchronized (mSurfaceHolder) {
				mScreenManager.setDimensions(width, height);
			}
		}

		/**
		 * Figures the ball state based on the passage of realtime.
		 * Does not invalidate(). Called at the start of draw().
		 * Detects the end-of-game and sets the UI to the next state.
		 */
		private void updatePhysics() {

			// Get the current time
			long now = System.currentTimeMillis();

			// Determine the time since last physics update
			long elapsed = now - mLastUpdateTime;

			// Accumulate time
			mGameTime += elapsed;

			while (elapsed-- > 0) {

				// Save the old velocity components
				double lOldXVelocity = mBall.xVelocity;
				double lOldYVelocity = mBall.yVelocity;

				// Determine total old speed
				double oldSpeed = Math.sqrt(lOldXVelocity*lOldXVelocity + lOldYVelocity*lOldYVelocity);
				
				// Calculate the new velocity components
				mBall.xVelocity += (mXGravity - mXTare) * GRAVITY_SCALAR * TIME_STEP * FRICTION;
				mBall.yVelocity += (mYGravity - mYTare) * GRAVITY_SCALAR * TIME_STEP * FRICTION;

				// Determine total new speed
				double newSpeed = Math.sqrt(mBall.xVelocity*mBall.xVelocity + mBall.yVelocity*mBall.yVelocity);
				
				// Check if we're slowing down
				if(newSpeed < oldSpeed) {
				
					// Make slowing down easier
					mBall.xVelocity *= newSpeed / oldSpeed;
					mBall.yVelocity *= newSpeed / oldSpeed;
					
					// Recalculate new speed
					newSpeed = Math.sqrt(mBall.xVelocity*mBall.xVelocity + mBall.yVelocity*mBall.yVelocity);
					
					// Stop ball if it's moving really really slow
					if(newSpeed < MIN_SPEED) {
						mBall.xVelocity = 0;
						mBall.yVelocity = 0;
					}
				}
				// Make sure ball isn't moving too fast
				else if(newSpeed > MAX_SPEED) {
					mBall.xVelocity *= MAX_SPEED / newSpeed;
					mBall.yVelocity *= MAX_SPEED / newSpeed;
				}
									
				// Move the ball
				mBall.xPosition += (mBall.xVelocity + lOldXVelocity)/2 * TIME_STEP;
				mBall.yPosition += (mBall.yVelocity + lOldYVelocity)/2 * TIME_STEP;

				handleWalls();

				handleFloors();
			}

			// Save the current update time
			mLastUpdateTime = now;
		}

		private void handleWalls() {

			int collision = Wall.NONE;
			double impulse = 0;

			int col = (int) mBall.xPosition;
			int row = (int) mBall.yPosition;

			int topType    = mLevel.hWalls[col][row].type;
			int bottomType = mLevel.hWalls[col][row+1].type;
			int leftType   = mLevel.vWalls[col][row].type;
			int rightType  = mLevel.vWalls[col+1][row].type;

			double dLeft   = mBall.xPosition - col;
			double dRight  = 1.0 - dLeft;
			double dTop    = mBall.yPosition - row;
			double dBottom = 1.0 - dTop;

			// Check Left Wall
			if(dLeft < Ball.RADIUS) {
				if (leftType == Wall.NORMAL) {

					mBall.xPosition = col + Ball.RADIUS;
					mBall.xVelocity *= BOUNCE;
					collision = Wall.NORMAL;
					impulse += Math.abs(mBall.xVelocity);
				}
				else if(leftType == Wall.LASER) {
					restartLevel();
					collision = Wall.LASER;
				}
			}

			// Check Right Wall
			else if(dRight < Ball.RADIUS) {

				if(rightType == Wall.NORMAL){

					mBall.xPosition = col - Ball.RADIUS +1;
					mBall.xVelocity *= BOUNCE;
					collision = Wall.NORMAL;
					impulse += Math.abs(mBall.xVelocity);
				}
				else if(rightType == Wall.LASER){
					restartLevel();
					collision = Wall.LASER;
				}
			}

			if(collision != Wall.LASER) {
				// Check Top Wall
				if(dTop < Ball.RADIUS) {
	
					if(topType == Wall.NORMAL){
	
						mBall.yPosition = row + Ball.RADIUS;
						mBall.yVelocity *= BOUNCE;
						collision = Wall.NORMAL;
						impulse += Math.abs(mBall.yVelocity);
					}
					else if(topType == Wall.LASER){
						restartLevel();
						collision = Wall.LASER;
					}
				}
	
				// Check Bottom Wall
				else if(dBottom < Ball.RADIUS) {
	
					if(bottomType == Wall.NORMAL){
	
						mBall.yPosition = row - Ball.RADIUS + 1;
						mBall.yVelocity *= BOUNCE;
						collision = Wall.NORMAL;
						impulse += Math.abs(mBall.yVelocity);
					}
					else if(bottomType == Wall.LASER) {
						restartLevel();
						collision = Wall.LASER;
					}
				}
			}

			if(collision == Wall.NONE){

				double dLeftTop = Math.sqrt(dLeft*dLeft + dTop*dTop);
				double dLeftBottom = Math.sqrt(dLeft*dLeft + dBottom*dBottom);
				double dRightTop = Math.sqrt(dRight*dRight + dTop*dTop);
				double dRightBottom = Math.sqrt(dRight*dRight + dBottom*dBottom);

				// Check Left Top Corner
				if(dLeftTop < Ball.RADIUS){

					int tlv = mLevel.vWalls[col][row-1].type;
					int tlh = mLevel.hWalls[col-1][row].type;

					// Check if neighbors have adjacent walls
					if((tlv != Wall.NONE) || (tlh != Wall.NONE)){

						// Resolve bounce to a side, rather than a corner
						if(dLeft > dTop){
							
							// Bounce off left
							mBall.xVelocity *= BOUNCE;
							collision = Wall.NORMAL;
							impulse += Math.abs(mBall.xVelocity);
						}
						else{

							// Bounce off top
							mBall.yVelocity *= BOUNCE;
							collision = Wall.NORMAL;
							impulse += Math.abs(mBall.yVelocity);
						}

						// Move ball to nearest point at radius from corner
						double theta = getTheta(dLeft, dTop);
						mBall.xPosition = col + getNewOpp(theta);
						mBall.yPosition = row + getNewAdj(theta);
					}
				}

				// Check Bottom Left Corner
				else if(dLeftBottom < Ball.RADIUS){

					int blv = mLevel.vWalls[col][row+1].type;
					int blh = mLevel.hWalls[col-1][row+1].type;

					// Check if neighbors have adjacent walls
					if((blv != Wall.NONE) || (blh != Wall.NONE)){
					
						// Resolve bounce to a side, rather than a corner
						if(dLeft > dBottom){

							// Bounce off left
							mBall.xVelocity *= BOUNCE;
							collision = Wall.NORMAL;
							impulse += Math.abs(mBall.xVelocity);
						}
						else{

							// Bounce off bottom
							mBall.yVelocity *= BOUNCE;
							collision = Wall.NORMAL;
							impulse += Math.abs(mBall.yVelocity);
						}

						// Move ball to nearest point at radius from corner
						double theta = getTheta(dLeft, dBottom);
						mBall.xPosition = col + getNewOpp(theta);
						mBall.yPosition = row - getNewAdj(theta) +1;
					}
				}

				// Check Top Right Corner
				else if(dRightTop < Ball.RADIUS){

					int trv = mLevel.vWalls[col+1][row-1].type;
					int trh = mLevel.hWalls[col+1][row].type;

					// Check if neighbors have adjacent walls
					if((trv != Wall.NONE) || (trh != Wall.NONE)){
						
						// Resolve bounce to a side, rather than a corner
						if(dRight > dTop){

							// Bounce off right
							mBall.xVelocity *= BOUNCE;
							collision = Wall.NORMAL;
							impulse += Math.abs(mBall.xVelocity);
						}
						else{

							// Bounce off top
							mBall.yVelocity *= BOUNCE;
							collision = Wall.NORMAL;
							impulse += Math.abs(mBall.yVelocity);
						}

						// Move ball to nearest point at radius from corner
						double theta = getTheta(dRight, dTop);
						mBall.xPosition = col - getNewOpp(theta) + 1;
						mBall.yPosition = row + getNewAdj(theta);
					}
				}

				// Check Bottom Right Corner
				else if(dRightBottom < Ball.RADIUS){

					int brv = mLevel.vWalls[col+1][row+1].type;
					int brh = mLevel.hWalls[col+1][row+1].type;

					// Check if neighbors have adjacent walls
					if((brv != Wall.NONE) || (brh != Wall.NONE)){
						
						// Resolve bounce to a side, rather than a corner
						if(dRight > dBottom){

							// Bounce off right
							mBall.xVelocity *= BOUNCE;
							collision = Wall.NORMAL;
							impulse += Math.abs(mBall.xVelocity);
						}
						else{

							// Bounce off bottom
							mBall.yVelocity *= BOUNCE;
							collision = Wall.NORMAL;
							impulse += Math.abs(mBall.yVelocity);
						}

						// Move ball to nearest point at radius from corner
						double theta = getTheta(dRight, dBottom);
						mBall.xPosition = col - getNewOpp(theta) + 1;
						mBall.yPosition = row - getNewAdj(theta) + 1;
					}
				} 
			}
			
			
			if(collision == Wall.NORMAL && impulse > IMPULSE_THRESHOLD)
				vibrate((long) (TIME_WALL_BASE + impulse));
			else if(collision == Wall.LASER)
				vibrate(TIME_LASER);
			
		}
		
		private void handleFloors() {
			
			int col = (int) mBall.xPosition;
			int row = (int) mBall.yPosition;

			Floor floor = mLevel.floors[col][row];
			
			// Check if the ball went into a pit
			if(floor.type == Floor.PIT) {
				restartLevel();
				vibrate(TIME_PIT);
			}
			
			else {
				
				if(!floor.equals(mBall.blockedPortal))
					// Remove any blocked portal
					mBall.blockedPortal = mBall.noPortal;

				// Check if the ball needs to be slowed
				if(floor.type == Floor.SLOW) {
					mBall.xVelocity *= DAMPEN;
					mBall.yVelocity *= DAMPEN;
					// TODO implement pattern vibrate with flag to prevent multiple layers
				}

				// Check if the ball needs to be sped up
				else if(floor.type == Floor.FAST) {
					mBall.xVelocity *= BOOST;
					mBall.yVelocity *= BOOST;
					// TODO implement pattern vibrate with flag to prevent multiple layers
				}

				// Check if they floor contains an active element
				if(floor.isVisible){
					
					// Check if the ball reached the end of the level
					if(floor.equals(mLevel.end)) {
						vibrate(TIME_END);
						setState(STATE_WIN);
						
					}

					// Check if the ball reached a portal
					else if(floor.isPortal()) {

						if(!mBall.blockedPortal.equals(floor)) {

							// Move the ball to the cooresponding portal
							mBall.xPosition = ((Portal) floor).other.col + 0.5;
							mBall.yPosition = ((Portal) floor).other.row + 0.5;

							// Block the ball from returning to through this portal immediately
							mBall.blockedPortal = ((Portal)floor).other;
							
							vibrate(TIME_PORTAL);
						}
					}

					else {

						// Check if the ball reached the key
						if(mLevel.key != null && floor.equals(mLevel.key)) {

							// Reveal End
							mLevel.key.isVisible = false;
							mLevel.end.isVisible = true;
							
							vibrate(TIME_KEY);
						}

						// Check if the ball triggered a button
						else if(floor.isButton()) {

							// Activate the corresponding portals
							((Button) floor).isVisible = false;
							((Button) floor).p1.isVisible = true;
							((Button) floor).p2.isVisible = true;
							
							vibrate(TIME_BUTTON);
						}
					}
				}
			}
		}

		private double getTheta(double opp, double adj){
			return Math.atan2(opp, adj);
		}

		private double getNewAdj(double theta){
			return Math.cos(theta)*Ball.RADIUS;
		}

		private double getNewOpp(double theta){
			return Math.sin(theta)*Ball.RADIUS;
		}
		
		private void vibrate(long time) {
			mVibrator.vibrate(time);
		}

		public void calibrate() {
			
			mXTare = mXGravity;
			mYTare = mYGravity;
			
			SharedPreferences mCalPrefs =
					mContext.getSharedPreferences(SharedConstants.CALIBRATION_PREFS, Context.MODE_PRIVATE);
			
			SharedPreferences.Editor mPrefsEditor = mCalPrefs.edit();
		
			mPrefsEditor.putFloat(SharedConstants.TARE_X, (float) mXTare);
			mPrefsEditor.putFloat(SharedConstants.TARE_Y, (float) mYTare);
			
			mPrefsEditor.commit();
			
		}

		public void onAccuracyChanged(Sensor sensor, int accuracy) {
			; // Do Nothing
		}


		public void onSensorChanged(SensorEvent event) { 
			
			if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
				
				long curTime = System.currentTimeMillis();
				
				if (mLastSensorUpdateTime == -1 || (curTime - mLastSensorUpdateTime) > 100) {
					
					mLastSensorUpdateTime = curTime;
		
					mXGravity = event.values[1];
					mYGravity = event.values[0];
					
//					// Triple check this. Brace yourself for disorientation.
//					switch(mRotationsClockwise) {
//					
//						case 0: { // Sensor thinks UP is UP, LEFT is LEFT
//							mXGravity = event.values[1] * (mMirrorY ? -1.0 : 1.0);
//							mYGravity = event.values[0];
//							break;
//						}
//					
//						case 1: { // Sensor thinks RIGHT is UP, UP is LEFT
//							mXGravity = event.values[0] * (mMirrorY ? -1.0 : 1.0);
//							mYGravity = -event.values[1];
//							break;
//						}
//						
//						case 2: { // Sensor thinks DOWN is UP, RIGHT is LEFT
//							mXGravity = -event.values[1] * (mMirrorY ? -1.0 : 1.0);
//							mYGravity = -event.values[0];
//							break;
//						}
//						
//						case 3: { // Sensor thinks LEFT is UP, DOWN is LEFT
//							mXGravity = -event.values[0] * (mMirrorY ? -1.0 : 1.0);
//							mYGravity = event.values[1];
//							break;
//						}
//					}
				}
			}
		}

		/**
		 * Restores game state from the indicated Bundle. Typically called when
		 * the Activity is being restored after having been previously
		 * destroyed.
		 * 
		 * @param savedState Bundle containing the game state
		 */
		public synchronized void restoreState(Bundle savedState) {
			synchronized (mSurfaceHolder) {
				setState(STATE_PAUSE);
				if(mBall==null){
					mBall=new Ball(0,0,0,0);
				}
				mBall.xPosition = savedState.getDouble(KEY_X);
				mBall.yPosition = savedState.getDouble(KEY_Y);
				mBall.xVelocity = savedState.getDouble(KEY_DX);
				mBall.yVelocity = savedState.getDouble(KEY_DY);
			}
		}

		/**
		 * Dump game state to the provided Bundle. Typically called when the
		 * Activity is being suspended.
		 * 
		 * @return Bundle with this view's state
		 */
		public Bundle saveState(Bundle map) {
			synchronized (mSurfaceHolder) {
				if (map != null) {
					map.putDouble(KEY_X, Double.valueOf(mBall.xPosition));
					map.putDouble(KEY_Y, Double.valueOf(mBall.yPosition));
					map.putDouble(KEY_DX, Double.valueOf(mBall.xVelocity));
					map.putDouble(KEY_DY, Double.valueOf(mBall.yVelocity));
				}
			}
			return map;
		}

		private void registerListener(){
			mSensorManager.registerListener(this, 
					mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), 
					SensorManager.SENSOR_DELAY_GAME);
		}

		private void unregisterListener(){
			mSensorManager.unregisterListener(this);
		}
	}

	private MrBall mActivity;

	/** Handle to the application context, used to e.g. fetch Drawables. */
	private Context mContext;

	/** The thread that actually draws the animation */
	private MrBallThread thread;

	/** Handle to the saved instance state */
	private Bundle savedInstanceState;

	public MrBallView(Context context, AttributeSet attrs) {
		super(context, attrs);

		// register our interest in hearing about changes to our surface
		SurfaceHolder holder = getHolder();
		holder.addCallback(this);

		mContext = context;

		setFocusable(true); // make sure we get key events
	}

	/**
	 * Fetches the animation thread corresponding to this MrBallView.
	 * 
	 * @return the animation thread
	 */
	public MrBallThread getThread() {
		return thread;
	}

	public void setSavedInstanceState(Bundle sis) {
		savedInstanceState = sis;
	}

	public void setActivity(MrBall activity) {
		mActivity = activity;
	}

	public void finish() {
		mActivity.finish();
	}

	/**
	 * Standard window-focus override. Notice focus lost so we can pause on
	 * focus lost. e.g. user switches to take a call.
	 */
	@Override
	public void onWindowFocusChanged(boolean hasWindowFocus) {
		if (!hasWindowFocus) 
			thread.pause();
	}

	/* Callback invoked when the surface dimensions change. */
	public void surfaceChanged(SurfaceHolder holder, int format, int width,
			int height) {
		thread.setSurfaceSize(width, height);
		thread.initialize(mActivity.getGame(), mActivity.getMode());
	}

	/*
	 * Callback invoked when the Surface has been created and is ready to be
	 * used.
	 */
	public void surfaceCreated(SurfaceHolder holder) {
		// start the thread here so that we don't busy-wait in run()
		// waiting for the surface to be created
		thread = new MrBallThread(this, holder, mContext);

		if (savedInstanceState == null) {
			// we were just launched: set up a new game
			thread.setState(MrBallThread.STATE_READY);
		} else {
			// we are being restored: resume a previous game
			thread.restoreState(savedInstanceState);

			// Clear the saved game so we don't restore it again
			savedInstanceState = null;
		}

		thread.setRunning(true);
		thread.start();
	}

	/*
	 * Callback invoked when the Surface has been destroyed and must no longer
	 * be touched. WARNING: after this method returns, the Surface/Canvas must
	 * never be touched again!
	 */
	public void surfaceDestroyed(SurfaceHolder holder) {
		// we have to tell thread to shut down & wait for it to finish, or else
		// it might touch the Surface after we return and explode
		boolean retry = true;
		thread.unregisterListener();
		thread.setRunning(false);
		while (retry) {
			try {
				thread.join();
				retry = false;
			} catch (InterruptedException e) {
			}
		}
	}
}
